package br.ufrj.dcc.ad.systems;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import br.ufrj.dcc.ad.args.ParametrosSimulacao;
import br.ufrj.dcc.ad.args.SimulationParameters;
import br.ufrj.dcc.ad.enums.DistributionType;
import br.ufrj.dcc.ad.enums.ExecutionType;
import br.ufrj.dcc.ad.enums.QueueType;
import br.ufrj.dcc.ad.enums.ServerType;
import br.ufrj.dcc.ad.event.ArriveEvent;
import br.ufrj.dcc.ad.event.CancelScheduledAtendmentsEvent;
import br.ufrj.dcc.ad.event.Event;
import br.ufrj.dcc.ad.event.ServerAtendmentEvent;
import br.ufrj.dcc.ad.generators.TimeGenerator;
import br.ufrj.dcc.ad.graphics.JFreeChartFacade;
import br.ufrj.dcc.ad.interfaces.IControlador;
import br.ufrj.dcc.ad.log.LoggerFactory;
import br.ufrj.dcc.ad.log.LoggerFactory.Type;
import br.ufrj.dcc.ad.metrics.SystemMetrics;

public class Controlador implements IControlador {
	private FilaDeEventos filaEventos;
	private Logger log = LoggerFactory.getLogger(LoggerFactory.Type.TIMED_STDOUT);
	private Logger debugLogger = LoggerFactory.getLogger(LoggerFactory.Type.DEBUG);
	private SimulationParameters parametroErrado;
	private double TEMPO_MAXIMO_SIMULACAO;
	
	public Controlador() {
		filaEventos = new FilaDeEventos();
	}
	
	@Override
	public void simula(Map<SimulationParameters, String> params) throws IllegalArgumentException {
		
		Event evento, proximoEvento;
		double eventClock = 0.0;
		List<Fila> filasDePrioridade = new ArrayList<Fila>();
		Fila filaGenerator = null;
		Class<?> classeGenerica = null;
		List<Event> eventosIvocacao = null;
		Servidor servidor;
		
		// Inicia filas.
		try {
			int i = -1;
			Constructor<?> construtor = null;
			TimeGenerator tempoServico = null;
			
			for(QueueType politicaFila : ParametrosSimulacao.getTiposFilas()){
				i++;
				//Inicializa gerador de serviço da fila.
				construtor = Class.forName(ParametrosSimulacao.getDistribuicoesServicosFilas().get(i).getNomeClasse()).getConstructor(double.class,double.class,double.class,double.class);
				tempoServico = (TimeGenerator)construtor.newInstance(ParametrosSimulacao.getPeriodosFilas().get(i),ParametrosSimulacao.getTaxasFilas().get(i),ParametrosSimulacao.getMediasFilas().get(i),ParametrosSimulacao.getDesviosPadroesFilas().get(i));
				//Inicializa fila.
				classeGenerica = Class.forName(politicaFila.getNomeClasse());
				filaGenerator = (Fila) classeGenerica.getConstructor(TimeGenerator.class).newInstance(tempoServico);
				filasDePrioridade.add(filaGenerator);
			}
		} catch (Exception e) {
			log.error("Erro !", e);
			// Erro na estanciação aqui não faz sentido continuar...
			return;
		}
		//Inicializa servidor com as suas filas de prioridade.
		servidor = new Servidor(filasDePrioridade);
		
		// Configurar gerador de tempo de chegadas.
		try {
			TimeGenerator chegadas = null;
			Constructor<?> construtor = Class.forName(ParametrosSimulacao.getDistribuicaoChegada().getNomeClasse()).getConstructor(double.class);
			chegadas = (TimeGenerator) construtor.newInstance(ParametrosSimulacao.getTaxaChegada());
			ParametrosSimulacao.setGeradorChegadas(chegadas);
		} catch (Exception e) {
			log.error("Erro !", e);
			// Erro na estanciação aqui não faz sentido continuar...
			return;
		}
		
		log.info("Cada rodada tera o maximo de " + TEMPO_MAXIMO_SIMULACAO + " tempo de simulacao");
		
		int j = 0;
		// DONE: Definir número de iterações aqui.
		for (j = 0; j < ParametrosSimulacao.getNumeroIteracoes(); j++) {
			
			//Limpeza de variaveis de controle.
			eventClock = 0.0;
			servidor.resetaServidor();
			filaEventos.clear();
			eventosIvocacao = null;
			ParametrosSimulacao.resetGeradorChegada();
			Cliente.resetSeqNum();
			
			//Adição do primeiro evento de chegada.
			evento = new ArriveEvent(servidor, ParametrosSimulacao.geraAmostraChegada(eventClock));// DONE: Pegar parâmetro do Tempo de consumo da primeira fila.
			addEventoNaFila(evento);
			
			log.info("Inicio da iteracao " + j);
			
			// Comeca a escrever no log alternativo.
			try {
				SystemMetrics.trocarLogClientes(j);
			} catch (IOException e) {
				log.error("Erro !", e);
				// Erro na troca do log, nao faz sentido continuar
				return;
			}
			
			int numeroClientesSistema = 0;
			
			while(continuaRodadaAteFicarEstacionario(eventClock)) {
				evento = getProximoEvento();
				eventClock = evento.getTempoOcorrencia();
				forneceInformacaoEvento(evento,servidor.getFilasDePrioridade());
				
				debugLogger.info(evento.toDebugString("Evento sendo tratado: ", ""));
				
				eventosIvocacao = processaEvento(evento);
				addEventosNaFila(eventosIvocacao);
				
				//Atualiza a média de clientes no sistema, caso houver um deslocamento do tempo.
				//Tem que ser aqui, pois lá em cima não temos certeza de qual será o verdadeiro próximo evento.
				proximoEvento = filaEventos.seeProximoEvento();
				if(proximoEvento == null || evento.getTempoOcorrencia() != proximoEvento.getTempoOcorrencia()){
//					debugLogger.info(evento.toDebugString("Evento sendo tratado: ", ""));
					numeroClientesSistema = SystemMetrics.getNumeroClientesSistema();
//					debugLogger.info(numeroClientesSistema + " clientes");
//					debugLogger.info("Porcentagem Estacionario " + SystemMetrics.getPorcentagemEstacionario(eventClock));
//					debugLogger.info("numeroClientes: " + numeroClientesSistema + ", media: " + SystemMetrics.getMediaNumeroClientesSistema());
					SystemMetrics.atualizaMediaNumeroClientesSistema(eventClock, numeroClientesSistema);
				}
			}
			
			// Volta a escrever no log original
			try {
				SystemMetrics.trocarLogClientes(null);
			} catch (IOException e) {
				log.error("Erro !", e);
				// Erro na troca do log, nao faz sentido continuar
				return;
			}
			SystemMetrics.clearMetricas();
			// Inicia uma nova rodada no log
			SystemMetrics.printSeparatorLogClientes();
			
			log.info("Estado estacionario da rodada " + j + " foi atingido no tempo: " + eventClock + " e media: " + SystemMetrics.getMediaNumeroClientesSistema());
			SystemMetrics.atualizaMediaTempoTransiente(eventClock, j);
			
			while (continuaRodadaAteTempoMaximo(eventClock)) {
				evento = getProximoEvento();
				eventClock = evento.getTempoOcorrencia();
				forneceInformacaoEvento(evento,servidor.getFilasDePrioridade());
				
				debugLogger.info(evento.toDebugString("Evento sendo tratado: ", ""));
				
				eventosIvocacao = processaEvento(evento);
				addEventosNaFila(eventosIvocacao);
				//Tem que ser aqui, pois lá em cima não temos certeza de qual será o verdadeiro próximo evento.
				proximoEvento = filaEventos.seeProximoEvento();
				if(proximoEvento == null || evento.getTempoOcorrencia() != proximoEvento.getTempoOcorrencia()){
					numeroClientesSistema = SystemMetrics.getNumeroClientesSistema();
//					debugLogger.info("Porcentagem Estacionario " + SystemMetrics.getPorcentagemEstacionario(eventClock) + " : " );
					SystemMetrics.atualizaMediaNumeroClientesSistema(eventClock, numeroClientesSistema);
//					debugLogger.info("numeroClientes: " + numeroClientesSistema + ", media: " + SystemMetrics.getMediaNumeroClientesSistema());
				}
				// DONE Implementar o resto do Plano de controle (simulslides3.pdf - página 15)
				// TODO Atualizar o diagrama no Google Docs para refletir isso.
			}
			
			log.info("Final da rodada " + j + " no tempo: " + eventClock + " e media: " + SystemMetrics.getMediaNumeroClientesSistema());
			
			debugLogger.info("Final da rodada " + j + " no tempo: " + eventClock + " e media de clientes: " + SystemMetrics.getMediaNumeroClientesSistema());
			
			SystemMetrics.addDuracaoRodada(eventClock);
			SystemMetrics.calculaMetricasRodada();
			SystemMetrics.printMetricasRodada();
			SystemMetrics.clearMetricasRodada();
		}
		
		SystemMetrics.printMetricasFinais();
		
		/**
		 * Plotando gráfico da evolução de clientes no sistema e tempo em filas.
		 */
		JFreeChartFacade.plotarGraficosMesmaImagem("Clientes Por Iteracao", "Tempo", "Clientes", "Rodada",
			SystemMetrics.VALUE_SEPARATOR, SystemMetrics.ROUND_SEPARATOR, j,
			"./MetricasClientesIteracao.txt", false);
//		JFreeChartFacade.plotarGraficos("MetricasClientesIteracao", "Tempo", "Clientes", "Clientes Por Iteracao",
//			SystemMetrics.VALUE_SEPARATOR, SystemMetrics.ROUND_SEPARATOR, j,
//			"./MetricasClientesIteracao.txt", false);
//		JFreeChartFacade.plotarGraficos("Tempo Total Por Iteracao", "Tempo Total", "P(T < Tempo Total)", "Tempo Total Por Iteracao",
//			SystemMetrics.VALUE_SEPARATOR, SystemMetrics.ROUND_SEPARATOR, j,
//			"./MetricasIteracao.txt", false);
		JFreeChartFacade.plotarGraficosMesmaImagem("Tempo Total Por Iteracao", "Tempo Total", "Rodada", "Tempo Total Por Iteracao",
			SystemMetrics.VALUE_SEPARATOR, SystemMetrics.ROUND_SEPARATOR, j,
			"./MetricasIteracao.txt", false);
		JFreeChartFacade.plotarGraficosIndexados("Clientes Por Iteracao", "Tempo", "Clientes", "Clientes Por Iteracao",
			SystemMetrics.VALUE_SEPARATOR, SystemMetrics.ROUND_SEPARATOR, j,
			"./MetricasClientesIteracao{ID}.txt", "{ID}", false);
	}
	
	// TODO : Para obter o tempo até ficar estacionário, melhorar o critério de conclusão que fica
	// no SystemMetrics e alterar esse método...
	private boolean continuaRodadaAteFicarEstacionario(double eventClock) {
//		return	( // Roda um valor mínimo até poder concluir algo.
//					eventClock <= ParametrosSimulacao.TEMPO_MINIMO_SIMULACAO &&
//					! filaEventos.isEmpty()
//				) ||
//				(
//					eventClock > ParametrosSimulacao.TEMPO_MINIMO_SIMULACAO &&
//					! SystemMetrics.isStationary(eventClock)
//				);
		return ! SystemMetrics.isStationary(eventClock);
	}
	
	private boolean continuaRodadaAteTempoMaximo(double eventClock) {
		return eventClock < TEMPO_MAXIMO_SIMULACAO;
	}
	
	private void logarEstadoDasFilas(List<Fila> filas) {
		// Log do estado das filas
		int i = -1;
		StringBuilder debugString = new StringBuilder();
		for (Fila fila : filas) {
			debugString.append("Fila " + (++i) + ": ");
			for (Cliente cliente : fila.clientes) {
				debugString.append(cliente.getNome() + " ");
			}
			debugString.append("\n");
		}
		debugLogger.info(debugString);
	}
	
	private void forneceInformacaoEvento(Event evento,List<Fila> filasPrioridade) {
		// Forneça ao evento de consumo qual o tempo do
		// próximo evento.
		if ((evento instanceof ServerAtendmentEvent)) {
			ServerAtendmentEvent serverAtendmentEvent = (ServerAtendmentEvent) evento;
			if (!filaEventos.isEmpty()) {
				serverAtendmentEvent.setTempoProximoEvento(filaEventos.seeProximoEvento().getTempoOcorrencia());
			} else {
				serverAtendmentEvent.setTempoProximoEvento(0.0);
			}
			if(ParametrosSimulacao.getTipoExecucao() == ExecutionType.DEBUG){
				logarEstadoDasFilas(filasPrioridade);
			}
			// Forneça a lista de eventos ao evento de cancelamento de
			// agendamento de consumo.
		} else if ((evento instanceof CancelScheduledAtendmentsEvent)) {
			CancelScheduledAtendmentsEvent cancelScheduledAtendmentsEvent = (CancelScheduledAtendmentsEvent) evento;
			if (! filaEventos.isEmpty()) {
				cancelScheduledAtendmentsEvent.setListaEventos(filaEventos.getEventos());
			}
		}
		
	}

	/**
	 * Inicializa parâmetros da simulação para que possam ser usados em toda
	 * aplicação.
	 * 
	 * @param params
	 *            Os parâmetros.
	 */
	public void inicializarParametrosSimulacao(Map<SimulationParameters, String> params) throws IllegalArgumentException {

		List<DistributionType> distribuicoesServico = new ArrayList<DistributionType>();
		List<QueueType> politicasFilas = new ArrayList<QueueType>();

		DistributionType distribuicao = DistributionType.EXPONENCIAL;
		List<Double> parametrosDistribuicao = new ArrayList<Double>();
		Double parametro = 0.0;
		QueueType politicaFila = null;
		SimulationParameters parametroAtual = null;
		
		try {
			parametroAtual = SimulationParameters.NUMBER_OF_ROUNDS;
			ParametrosSimulacao.setNumeroIteracoes(Integer.parseInt(params
					.get(parametroAtual)));
			ParametrosSimulacao.setDistribuicaoChegada(distribuicao);
			
			parametroAtual = SimulationParameters.EXECUTION_TYPE;
			ParametrosSimulacao.setTipoExecucao(ExecutionType.valueOf(params.get(parametroAtual).toUpperCase()));
			if(ParametrosSimulacao.getTipoExecucao() == ExecutionType.RELEASE){
				LoggerFactory.desactiveLogger(Type.DEBUG);
			}
			
			parametroAtual = SimulationParameters.ARRIVE_TAX;
			ParametrosSimulacao.setTaxaChegada(Double.parseDouble(params.get(parametroAtual)));
			
			parametroAtual = SimulationParameters.SERVER_TYPE;
			ParametrosSimulacao.setTipoServidor(ServerType.valueOf(params.get(parametroAtual).toUpperCase()));
			
			parametroAtual = SimulationParameters.SERVICE1_TYPE;
			distribuicao = DistributionType.valueOf(params.get(parametroAtual).toUpperCase());
			distribuicoesServico.add(distribuicao);
			
			parametroAtual = SimulationParameters.SERVICE2_TYPE;
			distribuicao = DistributionType.valueOf(params.get(parametroAtual).toUpperCase());
			distribuicoesServico.add(distribuicao);
	
			ParametrosSimulacao.setDistribuicoesServicosFilas(distribuicoesServico);
			
			parametroAtual = SimulationParameters.QUEUE1_TYPE;
			politicaFila = QueueType.valueOf(params.get(parametroAtual).toUpperCase());
			politicasFilas.add(politicaFila);
			
			parametroAtual = SimulationParameters.QUEUE2_TYPE;
			politicaFila = QueueType.valueOf(params.get(parametroAtual).toUpperCase());
			politicasFilas.add(politicaFila);
	
			ParametrosSimulacao.setTiposFilas(politicasFilas);
			
			parametroAtual = SimulationParameters.PERIOD1;
			parametro = params.get(parametroAtual) != null ? 1/Double.parseDouble(params.get(parametroAtual)) : 0.0;
			parametrosDistribuicao.add(parametro);
			
			parametroAtual = SimulationParameters.PERIOD2;
			parametro = params.get(parametroAtual) != null ? 1/Double.parseDouble(params.get(parametroAtual)) : 0.0;
			parametrosDistribuicao.add(parametro);
			
			ParametrosSimulacao.setPeriodosFilas(new ArrayList<Double>(parametrosDistribuicao));
			parametrosDistribuicao.clear();
			
			parametroAtual = SimulationParameters.TAX1;
			parametro = params.get(parametroAtual) != null ? Double.parseDouble(params.get(parametroAtual)) : 0.0;
			parametrosDistribuicao.add(parametro);
			
			parametroAtual = SimulationParameters.TAX2;
			parametro = params.get(parametroAtual) != null ? Double.parseDouble(params.get(parametroAtual)) : 0.0;
			parametrosDistribuicao.add(parametro);
			
			ParametrosSimulacao.setTaxasFilas(new ArrayList<Double>(parametrosDistribuicao));
			parametrosDistribuicao.clear();
			
			parametroAtual = SimulationParameters.MEAN1;
			parametro = params.get(parametroAtual) != null ? Double.parseDouble(params.get(parametroAtual)) : 0.0;
			parametrosDistribuicao.add(parametro);
			
			parametroAtual = SimulationParameters.MEAN2;
			parametro = params.get(parametroAtual) != null ? Double.parseDouble(params.get(parametroAtual)) : 0.0;
			parametrosDistribuicao.add(parametro);
			ParametrosSimulacao.setMediasFilas(new ArrayList<Double>(parametrosDistribuicao));
			parametrosDistribuicao.clear();
			
			parametroAtual = SimulationParameters.DP1;
			parametro = params.get(parametroAtual) != null ? Double.parseDouble(params.get(parametroAtual)) : 0.0;
			parametrosDistribuicao.add(parametro);
			
			parametroAtual = SimulationParameters.DP2;
			parametro = params.get(parametroAtual) != null ? Double.parseDouble(params.get(parametroAtual)) : 0.0;
			parametrosDistribuicao.add(parametro);
			
			ParametrosSimulacao.setDesviosPadroesFilas(new ArrayList<Double>(parametrosDistribuicao));
			
			parametroAtual = SimulationParameters.EXECUTION_TIME;
			TEMPO_MAXIMO_SIMULACAO = Double.parseDouble(params.get(parametroAtual));
			
			parametroAtual = null;
		} catch(NullPointerException e) {
			parametroErrado = parametroAtual;
			throw new IllegalArgumentException(e);
		}
	}

	@Override
	public void addEventoNaFila(Event event) {
		filaEventos.addEventoNaFila(event);
	}

	@Override
	public void addEventosNaFila(List<Event> events) {
		filaEventos.addEventosNaFila(events);
	}

	@Override
	public Event getProximoEvento() {
		return filaEventos.getProximoEvento();
	}

	@Override
	public List<Event> processaEvento(Event event) {
		return event.executaAcao();
	}
	
	public SimulationParameters getParametroErrado() {
		return parametroErrado;
	}
	
	@Deprecated
	public void shutdown() {
	}
}
